require 'set'

def build_home
  $model.start_operation "build_home"

  # instantiate home
  technology=JoistBasedTechnology.new
  #home = Home.new(technology, 1, 2*technology.externalWallThickness+4*technology.plates_dim_a, 2*technology.externalWallThickness+8*technology.plates_dim_b) # JoistBasedTechnology.new.joist_width
  home = Home.new(technology, 1, 10.m, 8.m) # JoistBasedTechnology.new.joist_width

  #make some manual changes
  #home.walls['s'] = GlassFacade.new home
  #home.walls['w'] = GlassFacade.new home
  #home.walls['n'] = GlassFacade.new home
  #home.walls['e'] = GlassFacade.new home

  # display
  home.prepare
  #home.floors[0].test_cell($model.entities, 0, 1)
  home.make_group($model.entities)
  
  $model.commit_operation
  
  # print stats
  print_stats( home.living_area_m2 )

end

World_directions = ['w', 'e', 's', 'n']

class Home
  
  attr_accessor :floors, :walls, :dim_x, :dim_y, :technology, :living_area_m2

  def initialize( technology, floors_cnt, dim_x, dim_y )
    @technology = technology
    @dim_x = dim_x
    @dim_y = dim_y
    @living_area_m2=0
    
    @floors = Array.new floors_cnt
    (0..@floors.size-1).each do |f|
      floor = Floor.new self
      if f==0
        floor.roles.add( Floor::RoleBottom )
      end
      if f==@floors.size-1
        floor.roles.add( Floor::RoleTop )
      end
      
      @floors[f] = floor
    end
    
    # walls ordering w,e,s,n is important for dynamic code calculating living area and wall supporting offsets, 
    # do not change this
    @walls = Hash.new
    World_directions.each {|world_direction|
      @walls[world_direction]=OuterLoadBearingInsulatedWall.new self
    }
  end
  
  def prepare()
    @floors.each do |floor|
      floor.prepare
    end
  end
  
  def make_group(entities) 
    #name = 'home_'+@floors.size.to_s+"_"+dim_to_s(@dim_x)+"_"+dim_to_s(@dim_y)
    
    group=entities.add_group

    @floors.each do |floor|
      floor_name = floor.make_group(group.entities)
    end
    
    return group
  end
end

class Wall
  attr_accessor :home, :thickness, :support_offset
  
  def initialize(home)
    @home = home
  end
  
  def technology()
    return @home.technology
  end
end

class OuterLoadBearingInsulatedWall < Wall
  def initialize(home)
    super(home)
    @thickness = technology.externalWallThickness
    @support_offset = technology.inner_wall_thickness
  end
end

class GlassFacade < Wall
  def initialize(home)
    super(home)
    @thickness= technology.joist_width / 2
    @support_offset = technology.joist_width / 2
  end
end

class InnerNonLoadBearingWall < Wall
  def initialize(home)
    super(home)
    @thickness = 0
    @support_offset = technology.joist_width / 2
  end
end

class Floor
  
  RoleBottom='b'
  RoleTop='t'
  
  attr_accessor :home, :group, :living_area_m2, :living_area_x, :living_area_y, :cells, :roles
  
  def initialize(home)
    @home = home
    @roles = Set.new
  end

  def prepare
    @living_area_x = @home.dim_x - @home.walls['w'].thickness - @home.walls['e'].thickness
    @living_area_y = @home.dim_y - @home.walls['s'].thickness - @home.walls['n'].thickness
    debug("@living_area_x; "+@living_area_x.to_mm.to_s+", @living_area_y: "+@living_area_y.to_mm.to_s)
    assert_positiv("living_area_x", @living_area_x, RoundTollerance)
    assert_positiv("living_area_y", @living_area_y, RoundTollerance)
    
    @living_area_m2 = @living_area_x.to_m * @living_area_y.to_m
    debug("floor living area m2: "+@living_area_m2.to_s)
    @home.living_area_m2 += @living_area_m2
    
    # make symetric cells with columns in the middle
    x_cells_cnt = (@living_area_x/technology.joist_optimal_length.to_f-RoundTollerance).ceil
    x_cells_size = @living_area_x/x_cells_cnt
    y_cells_cnt = (@living_area_y/technology.joist_optimal_length.to_f-RoundTollerance).ceil
    y_cells_size = @living_area_y/y_cells_cnt
    debug("constructing #{x_cells_cnt}x#{y_cells_cnt} cells, optimal beam length #{technology.joist_optimal_length.to_mm.to_s}")
    

    #cell.walls['w'] = InnerNonLoadBearingWall.new @home
    #cell.walls['s'] = InnerNonLoadBearingWall.new @home
    @cells = Array.new x_cells_cnt
    (0..x_cells_cnt-1).each do |x|
      @cells[x] = Array.new y_cells_cnt
      (0..y_cells_cnt-1).each do |y|
        cell= FloorCell.new self
        cell.walls = Hash.new 4
        World_directions.each do |direction|
          cell.walls[direction] = InnerNonLoadBearingWall.new @home
        end
        cell.pos_x=x*x_cells_size
        cell.pos_y=y*y_cells_size
        cell.cell_dim_x = x_cells_size
        cell.cell_dim_y = y_cells_size
        @cells[x][y]=cell
      end
    end
    # set walls as defined in @home
    (0..x_cells_cnt-1).each do |x|
      @cells[x][0].walls['s']=@home.walls['s']
      @cells[x][y_cells_cnt-1].walls['n']=@home.walls['n']
    end
    (0..y_cells_cnt-1).each do |y|
      @cells[0][y].walls['w']=@home.walls['w']
      @cells[x_cells_cnt-1][y].walls['e']=@home.walls['e']
    end
  end

  def technology()
    return @home.technology
  end  

  def make_group(entities)
    
    if @roles.empty?
      raise Exception, "at least one role must be set"
    end
    
    @group = entities.add_group
    @cells.each do |cells_x|
      cells_x.each do |cell|
        render_cell(entities, cell)
      end
    end
    return @group
  end

  def test_cell(entities, x, y)
    cell=@cells[x][y]
    render_cell(entities, cell)
  end

  def render_cell(entities, cell)
    tr = Geom::Transformation.translation [ cell.pos_x, cell.pos_y, 0 ]
    cell.make_group(entities).move!( tr )
  end
end

class FloorCell
  
  attr_accessor :floor, :pos_x, :pos_y, :cell_dim_x, :cell_dim_y, :walls
  
  attr_accessor :living_area_w_support_offset, :living_area_e_support_offset, :living_area_s_support_offset, :living_area_n_support_offset
  attr_accessor :living_area_w, :living_area_e, :living_area_s, :living_area_n, :living_area_x, :living_area_y
  attr_accessor :platform_w, :platform_e, :platform_s, :platform_n, :platform_x, :platform_y

  # some strict private variables 
  @group
  @debug_add_plates #for some debugging
  
  def initialize(floor)
    @floor=floor
    @debug_add_plates = !true
  end

  def technology()
    return @floor.technology
  end

  def set_edge_vars( world_direction, start_coordinate, plus_or_minus )
      wall = @walls[world_direction]
      # living_area_[w|e|s|n]_support_offset
      eval("@living_area_"+world_direction+"_support_offset = "+plus_or_minus+wall.support_offset.to_f.to_s)
      debug( "@living_area_"+world_direction+"_support_offset: "+eval("@living_area_"+world_direction+"_support_offset.to_mm.to_s")+" mm")
      # @living_area_[w|e|s|n]
      eval("@living_area_"+world_direction+" = "+start_coordinate.to_f.to_s)
      debug( "@living_area_"+world_direction+": "+eval("@living_area_"+world_direction+".to_mm.to_s")+" mm")
      # patform_[w|e|s|n]
      eval( "@platform_"+world_direction+" = @living_area_"+world_direction+"+"+"@living_area_"+world_direction+"_support_offset")
      debug( "@platform_"+world_direction+": "+eval("@platform_"+world_direction+".to_mm.to_s")+" mm")
  end

  def make_group(entities)
    #name = 'floor_'+@home.floors.size.to_s+"_"+dim_to_s(@home.dim_x)+"_"+dim_to_s(@home.dim_y)
    @group = entities.add_group
    
    ######################################
    # determine coordinates of living area and ....
    ######################################
    # determine edge-offsets for wall-supports ( floor-wall overlaps )
    # offsets are positive or negative, depending on offset direction
    ######################################
    # variable declarations are not necessary, but for code auto-completition quite useful
    # 
    
    set_edge_vars('w', 0,           '-')
    set_edge_vars('e', @cell_dim_x,'+')
    set_edge_vars('s', 0,           '-')
    set_edge_vars('n', @cell_dim_y,'+')

    @platform_x=platform_e-platform_w
    @platform_y=platform_n-platform_s

#    @living_area_m2= (@living_area_e-@living_area_w).to_m * (@living_area_n-@living_area_s).to_m
#    debug( "platform's @living_area_m2: "+@living_area_m2.to_s )
#    @home.living_area_m2 += @living_area_m2
    
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # add construction elements from bottom to up
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    
    ################################################
    # add floor plates
    ################################################
    #add_floor_plates
    
    ################################################
    # add horisontal beams parallel to X axis ( X beams )
    ################################################
    add_horisontal_x_beams_and_plates
    
    ################################################
    # add horisontal Y beams
    ################################################
    add_horisontal_y_beams
    
    return @group
  end
  
  def add_horisontal_x_beams_and_plates()
    
    # first calculate X beams length
    # start assumption is a situation where both sides are inner walls (supporting column)
    x_beam_length = @cell_dim_x - technology.joist_width
    x_beam_outer_length = @cell_dim_x
    x_beam_x_pos = technology.joist_width/2
    x_beam_outer_x_pos = x_beam_x_pos
    x_beam_outer_s_y_pos=-@walls['s'].support_offset+technology.joist_width/2
    x_beam_outer_n_y_pos=@cell_dim_y+@walls['n'].support_offset-technology.joist_width/2

    # add wall supports
    x_beam_outer_length += walls['w'].support_offset-technology.joist_width
    x_beam_outer_x_pos -= walls['w'].support_offset-technology.joist_width/2
    
    x_beam_outer_length += walls['e'].support_offset-technology.joist_width
    
    rot_x = Geom::Transformation.rotation ORIGIN, [1,0,0], Math::PI/2
    rot_y = Geom::Transformation.rotation ORIGIN, [0,1,0], -Math::PI/2
    tr_origin = (Geom::Transformation.translation [ 0, technology.joist_width , 0 ]) * rot_x * rot_y
    
    y=@living_area_s
    
    joist_name = make_joist("joist", technology.joist_width, technology.joist_heigth, x_beam_length)
    joist_outer_name = make_joist("joist", technology.joist_width, technology.joist_heigth, x_beam_outer_length)

    # add first south X beam supporting wall if needed

    #if @walls['s'].class != InnerNonLoadBearingWall
      debug("adding outer X beam on y pos "+x_beam_outer_s_y_pos.to_mm.to_s)
      tr = Geom::Transformation.translation [ x_beam_outer_x_pos, x_beam_outer_s_y_pos-technology.joist_width/2, cealing_z_end ]
      @group.entities.add_instance( $model.definitions[joist_outer_name], tr*tr_origin )
    #else
    #  rot_x2 = Geom::Transformation.rotation ORIGIN, [1,0,0], -Math::PI/2
    #  rot_y2 = Geom::Transformation.rotation ORIGIN, [0,1,0], +Math::PI/2
    #  add_strong_supporting_beam( "adding south strong X beam", rot_x2*rot_y2, x_beam_outer_length, x_beam_outer_x_pos, x_beam_outer_s_y_pos+technology.joist_width/2 )
    #end

    y = 0 # beam y position of X beam middle ( not the edge )
    plate_width=-1
    while (@cell_dim_y-y).abs > RoundTollerance
      # calculate next plate width
      
      plate_width=min technology.plates_dim_b, @cell_dim_y-y

      debug("adding plates strip width "+plate_width.to_mm.to_s)
      add_plates_in_x_direction( tr_origin, plate_width, @living_area_w, y )
      # first beam is always added, with exception if first bean overlaps outer beam
      y_beam=y
      if (x_beam_outer_s_y_pos-y_beam).abs > RoundTollerance
        debug("adding X beam on y pos "+y_beam.to_mm.to_s)
        add_x_beam( tr_origin, joist_name, x_beam_x_pos, y_beam )
      else
        debug("ommiting overlaping X beam on y pos "+y_beam.to_mm.to_s )
      end
      
      # add next beams in whole distance
      while y_beam-y <= plate_width - technology.joist_distance - RoundTollerance
        y_beam += technology.joist_distance
        debug("adding X beam on y pos "+y_beam.to_mm.to_s)
        # add supporting Y edge beams if needed
        add_y_edge_supporting_joists(tr_origin, technology.joist_distance-technology.joist_width, @living_area_w, y_beam)
        add_x_beam( tr_origin, joist_name, x_beam_x_pos, y_beam )
        debug("y_beam-y:#{(y_beam-y).to_mm.to_s} <= (plate_width - technology.joist_distance - RoundTollerance):#{(plate_width - technology.joist_distance - RoundTollerance).to_mm.to_s}")
      end
      
      y += plate_width

      # last beam only if outer bean does not overlaps
      add_y_edge_supporting_joists(tr_origin, y-y_beam-technology.joist_width, @living_area_w, y)
      if (x_beam_outer_n_y_pos-y).abs > RoundTollerance and @cell_dim_y-y < RoundTollerance
        debug("adding last X beam on y pos "+y.to_mm.to_s)
        add_x_beam( tr_origin, joist_name, x_beam_x_pos, y )
      end
    end
    
    # add north outer X beam supporting wall
    if @walls['n'].class != InnerNonLoadBearingWall
      debug("adding outer X beam on y pos "+x_beam_outer_n_y_pos.to_mm.to_s)
      tr = Geom::Transformation.translation [ x_beam_outer_x_pos, x_beam_outer_n_y_pos-technology.joist_width/2, cealing_z_end ]
      @group.entities.add_instance( $model.definitions[joist_outer_name], tr*tr_origin )
    end
    
  end

  def add_y_edge_supporting_joists(tr_origin, length, x_start_pos, y_pos)
    # assert length is positiv
    assert_positiv("length", length, RoundTollerance)
    
    # do not make to short supporting beams
    if length <= 5*technology.floor_plates_strength
      return
    end
    
    # add joist supporting plates on Y edge if this plate is not last plate
    rot_z = Geom::Transformation.rotation ORIGIN, [0,0,1], -Math::PI/2
    x=x_start_pos
    while @cell_dim_x-x > RoundTollerance
      plate_length = min @cell_dim_x-x, technology.plates_dim_a
      next_length = @cell_dim_x-x-plate_length
      if next_length > RoundTollerance
        x_candidate=x+plate_length-technology.joist_width/2
        if x_candidate+3*technology.joist_width/2 > @cell_dim_x+RoundTollerance
          x_candidate=@cell_dim_x-3*technology.joist_width/2
        end
        joist_name = make_joist("joist", technology.joist_width, technology.joist_heigth, length)
        tr = Geom::Transformation.translation [ x_candidate, y_pos-technology.joist_width/2, cealing_z_end ]
        @group.entities.add_instance( $model.definitions[joist_name], tr*rot_z*tr_origin )
        debug("added Y-edge supporting beam on y-pos "+y_pos.to_mm.to_s+" and x-pos "+x_candidate.to_mm.to_s)
      end
      x += plate_length
    end
  end
  
  def add_plates_in_x_direction( tr_origin, plate_width, x_start_pos, y_pos )
    x=x_start_pos
    while (@cell_dim_x-x).abs > RoundTollerance
      dxy = Geom::Transformation.translation [ x, y_pos, 0 ]
      # meke next plate with maximal or rest length
      plate_length = min @cell_dim_x-x, technology.plates_dim_a
      
      # make plate shorter if next plate would be to short
      plate_length_rest= @cell_dim_x-(x+plate_length)
      if plate_length_rest > RoundTollerance and plate_length_rest < 3*technology.joist_width/2+RoundTollerance
        plate_length=@cell_dim_x-x-3*technology.joist_width/2
      end
      
      # cealing
      cealing_thickness=0
      # first plate level
      dz = Geom::Transformation.translation [ 0, 0, 0 ]
      plate_name = make_block_component2( 'cealing_plate', plate_length, plate_width, technology.cealing_plates_strength, $materials['gips_1'] )
      if @debug_add_plates
        @group.entities.add_instance( $model.definitions[plate_name], dxy*dz )
      end
      cealing_thickness += technology.cealing_plates_strength
      
      # assert if cealing_z_end returns correct value
      if (cealing_z_end-cealing_thickness).abs > RoundTollerance
        raise Exception, "cealing_z_end=#{cealing_z_end.to_mm} differs from cealing_thickness=#{cealing_thickness.to_mm}"
      end
      
      # main floor plates
      floor_z_start = cealing_z_end + technology.joist_heigth
      
      # first load bearing plate
      dz  = Geom::Transformation.translation [ 0, 0, floor_z_start ]
      plate_name = make_block_component2( 'floor_plate', plate_length, plate_width, technology.floor_plates_strength, $materials['OSB_1'] )
      if @debug_add_plates
        @group.entities.add_instance( $model.definitions[plate_name], dxy*dz )
      end
      
      x += plate_length
    end
  end

  def cealing_z_end
    return technology.cealing_plates_strength
  end

  def add_x_beam( tr_origin, joist_name, x_beam_x_pos, y )
    tr = Geom::Transformation.translation [ x_beam_x_pos, y-technology.joist_width/2, cealing_z_end ]
    @group.entities.add_instance( $model.definitions[joist_name], tr*tr_origin )
  end

  def add_horisontal_y_beams()

    rot_x = Geom::Transformation.rotation ORIGIN, [1,0,0], Math::PI/2

    # first calculate Y beams length
    # start assumption is a situation where both sides are inner walls (supporting column)
    y_beam_length = @cell_dim_y+technology.joist_width
    
    y_beam_y_pos = -technology.joist_width/2

    # add walls support if needed
    y_beam_length += @walls['s'].support_offset-2*technology.joist_width
    y_beam_y_pos -= @walls['s'].support_offset-3*technology.joist_width/2
    y_beam_length += @walls['n'].support_offset-technology.joist_width
    
    y_beam_outer_length = y_beam_length+2*technology.joist_width
    y_beam_outer_y_pos  = y_beam_y_pos-technology.joist_width
    
    y_beam_outer_w_x_pos = -@walls['w'].support_offset+technology.joist_width/2
    y_beam_outer_e_x_pos = @cell_dim_x + @walls['e'].support_offset-technology.joist_width/2
    
    # make corection for if some of walls are inner walls
    if @walls['s'].class == InnerNonLoadBearingWall
      y_beam_outer_length -= technology.joist_width/2
      y_beam_outer_y_pos += technology.joist_width/2
    end
    if @walls['n'].class == InnerNonLoadBearingWall
      y_beam_outer_length -= technology.joist_width/2
    end
    
    #
    # add inner beams
    #

    # west Y inner beam
    y_beam_w_x_pos=0
    if (y_beam_outer_w_x_pos-y_beam_w_x_pos).abs > RoundTollerance
      tr=Geom::Transformation.translation [ -technology.joist_width/2, y_beam_y_pos, cealing_z_end ];
      joist_name = make_joist("joist", technology.joist_width, technology.joist_heigth, y_beam_length)
      @group.entities.add_instance( $model.definitions[joist_name], tr*rot_x )
    end
    # east Y inner beam
    y_beam_e_x_pos=@cell_dim_x
    if (y_beam_outer_e_x_pos-y_beam_e_x_pos).abs > RoundTollerance
      tr=Geom::Transformation.translation [ y_beam_e_x_pos-technology.joist_width/2, y_beam_y_pos, cealing_z_end ];
      joist_name = make_joist("joist", technology.joist_width, technology.joist_heigth, y_beam_length)
      @group.entities.add_instance( $model.definitions[joist_name], tr*rot_x )
    end    
    
    #
    # add outer Y beams
    #

    # west Y outer beam
    joist_outer_name = make_joist("joist", technology.joist_width, technology.joist_heigth, y_beam_outer_length)
    if @walls['w'].class != InnerNonLoadBearingWall
      add_y_beam( 'adding west beam Y', rot_x, joist_outer_name, y_beam_outer_w_x_pos, y_beam_outer_y_pos )
    else
      rot_x2 = Geom::Transformation.rotation ORIGIN, [1,0,0], -Math::PI/2
      add_strong_supporting_beam( "adding west strong Y beam", rot_x2, y_beam_outer_length, y_beam_outer_w_x_pos-technology.joist_width/2, y_beam_outer_y_pos )
    end
    # east Y beam
    if @walls['e'].class != InnerNonLoadBearingWall
      add_y_beam( 'adding east beam Y', rot_x, joist_outer_name, y_beam_outer_e_x_pos, y_beam_outer_y_pos )
    end
  end
  
  def add_y_beam( debug_msg, tr_origin, joist_name, y_beam_x_pos, y )
    debug( debug_msg )
    tr = Geom::Transformation.translation [ y_beam_x_pos-technology.joist_width/2, y, cealing_z_end ]
    @group.entities.add_instance( $model.definitions[joist_name], tr*tr_origin )
  end
  
  def add_strong_supporting_beam( debug_msg, tr_origin, length, x, y )
    debug( debug_msg )
    tr=Geom::Transformation.translation [ x, y, cealing_z_end+technology.joist_heigth ];
    beam_name = make_block_component2("supporting_y_beam", technology.joist_width, technology.joist_heigth, length, $materials['wood_1'])
    @group.entities.add_instance( $model.definitions[beam_name], tr*tr_origin )
  end

end

